/*
 * Copyright (c) 2008-20018 Saša Stamenković <umpirsky@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *    * Redistributions of source code must retain the above copyright notice,
 *      this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright notice,
 *    	this list of conditions and the following disclaimer in the documentation
 *    	and/or other materials provided with the distribution.
 *    * Neither the name of the <ORGANIZATION> nor the names of its contributors
 *    	may be used to endorse or promote products derived from this software without
 *    	specific prior written permission.
 *
 *    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 *    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 *    IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 *    INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 *    NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 *    OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 *    WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package umpirsky;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;

import umpirsky.json.JSONArray;
import umpirsky.json.JSONException;
import umpirsky.json.JSONObject;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Observable;
import java.util.Observer;

import javax.swing.JApplet;
import javax.swing.JFileChooser;
import javax.swing.UIManager;

/**
 * Multiple file upload applet
 *
 * @author Saša Stamenković <umpirsky@gmail.com>
 */
public class UmpirskyUploader extends JApplet implements Observer {
	/**
	 * Serial
	 */
	private static final long serialVersionUID = 1L;

	/**
	 * File chooser
	 */
	private UmpirskyFileChooser fileChooser;

	/**
	 * List of files to be ulpoaded to server (url)
	 */
	private static ArrayList<File> files = new ArrayList<File>();

	/**
	 * URL location where to upload files
	 */
	private String url = "http://localhost/umpirskyuploader/";

	/**
	 * Maximum number of files that can be selected
	 */
	private short numberOfFilesMax = 0;

	/**
	 * Maximum MBs allowed for upload
	 */
	private short uploadMbMax = 0;

	/**
	 * Maximum MBs allowed for one file
	 */
	private short fileMbMax = 0;

	/**
	 * Is multiple selection enabled
	 */
	private boolean multipleSelection = true;

	/**
	 * Is file preview enabled
	 */
	private boolean previewEnabled = false;

	/**
	 * GUI look and feel
	 */
	private String lookAndFeel = UIManager.getSystemLookAndFeelClassName();

	/**
	 * Indicates if file chooser has file filter
	 */
	private boolean filterEnabled = false;

	/**
	 * File filter description
	 */
	private String filterDescription = "Umpirsky files";

	/**
	 * Comma separated file filter extensions
	 */
	private String filterExtensions = "ump";

	/**
	 * Override applet method, called on applet initialization. Configure applet properties.
	 */
	@Override
	public void init() {
		// Set security manager (required for IE)
		System.setSecurityManager(new UmpirskySecurityManager());

		// Get look and feel
		String parameter = getParameter("look_and_feel");
		if (null != parameter && !parameter.isEmpty()) {
			this.lookAndFeel = parameter;
		}

		// Get url
		parameter = getParameter("url");
		if (null != parameter && !parameter.isEmpty()) {
			this.url = parameter;
		}

		// Get multiple selection
		parameter = getParameter("multiple_selection");
		if (null != parameter && !parameter.isEmpty()) {
			this.multipleSelection = Boolean.parseBoolean(parameter);
		}

		// Get preview enabled
		parameter = getParameter("preview_enabled");
		if (null != parameter && !parameter.isEmpty()) {
			this.previewEnabled = Boolean.parseBoolean(parameter);
		}

		// Get maximum number of files
		parameter = getParameter("number_of_files_max");
		if (null != parameter && !parameter.isEmpty()) {
			this.numberOfFilesMax = Short.parseShort(parameter);
		}

		// Get maximum upload size
		parameter = getParameter("upload_mb_max");
		if (null != parameter && !parameter.isEmpty()) {
			this.uploadMbMax = Short.parseShort(parameter);
		}

		// Get maximum file size
		parameter = getParameter("file_mb_max");
		if (null != parameter && !parameter.isEmpty()) {
			this.fileMbMax = Short.parseShort(parameter);
		}

		// Get has filter
		parameter = getParameter("filter_enabled");
		if (null != parameter && !parameter.isEmpty()) {
			this.filterEnabled = Boolean.parseBoolean(parameter);
		}

		// Get filter name
		parameter = getParameter("filter_description");
		if (null != parameter && !parameter.isEmpty()) {
			this.filterDescription = parameter;
		}

		// Get extensions
		parameter = getParameter("filter_extensions");
		if (null != parameter && !parameter.isEmpty()) {
			this.filterExtensions = parameter;
		}

		// Applet loaded, trigger event
		try {
			getAppletContext().showDocument(new URL("javascript:umpirsky.events.onLoad()"));
		} catch (MalformedURLException e) { }
	}

	/**
	 * Provides files list in json format
	 *
	 * @return String json files list
	 */
	public String getFiles() {
		JSONArray jsonFilesList = new JSONArray();
		for (File file : files) {
			JSONObject jsonFile = new JSONObject();
			try {
				jsonFile.put("id", files.indexOf(file));
				jsonFile.put("name", file.getName());
				jsonFile.put("size", file.length()/1024);
				jsonFile.put("date", new Date(file.lastModified()).toString());
			} catch (JSONException e) {
				log(e);
			}
			jsonFilesList.put(jsonFile);
		}

		return jsonFilesList.toString();
	}

	/**
	 * Get number of files in file list
	 *
	 * @return int number of files
	 */
	public static int getFilesCount() {
		return files.size();
	}

	/**
	 * Get size of files from files list in MB
	 *
	 * @return short number of files
	 */
	public static short getFilesSize() {
		long size = 0;
		for (File file : files) {
			size += file.length();
		}

		// Convert to int MB
		return (short) (size/(1024*1024));
	}

	/**
	 * Security wrapper for browseHandler. Shows file chooser and provides json list of choosen files
	 */
	public void browse() {
		new Thread() {
			@Override
			public void run() {
				try {
					// Browse
					browseHandler();

					// Triggers file list updated method
					fileListUpdated();
				} catch (Exception e) {
					log(e);
				}
			}
		}.start();
	}

	/**
	 * Opens new thread for upload handler (just not to block browser)
	 */
	public void upload() {
		new Thread() {
			@Override
			public void run() {
				try {
					uploadHandler();
				} catch (Exception e) {
					log(e);
					try {
						getAppletContext().showDocument(new URL("javascript:umpirsky.events.uploadFinished('" + "-1" + "')"));
					} catch (MalformedURLException me) { }
				}
			}
		}.start();
	}

	/**
	 * Removes file from file list
	 *
	 * @param index index of file in arrayList
	 */
	public void removeFile(int index) {
		files.remove(index);
		fileListUpdated();
	}

	/**
	 * Clear all files from files list
	 */
	public void clearFiles() {
		files.clear();
		fileListUpdated();
	}

	/**
	 * Opens file chooser, and gets selected files for upload
	 */
	private void browseHandler() {
		// If file chooser is already shown, just return (prevent multiple file choosers to be shown)
		if (null != this.fileChooser && this.fileChooser.isShowing()) {
			return;
		}

		// Initialize file chooser
		if (null == this.fileChooser) {
			// Set look and feel
			try {
				javax.swing.UIManager.setLookAndFeel(this.lookAndFeel);
			} catch(Exception e) {
				log(e);
			}

			// Initialize file chooser
			this.fileChooser = new UmpirskyFileChooser();
			this.fileChooser.setMultiSelectionEnabled(this.multipleSelection);

			// Set preview accesory
			if (this.previewEnabled) {
				 UmpirskyFilePreview filePreview = new UmpirskyFilePreview(this.fileChooser);
				 this.fileChooser.setAccessory(filePreview);
				 this.fileChooser.addPropertyChangeListener(JFileChooser.SELECTED_FILES_CHANGED_PROPERTY, filePreview);
			}

			// Set limits
			this.fileChooser.setNumberOfFilesMax(this.numberOfFilesMax);
			this.fileChooser.setUploadMbMax(this.uploadMbMax);
			this.fileChooser.setFileMbMax(this.fileMbMax);

			// Add file filter
			if (this.filterEnabled) {
				UmpirskyFileFilter filter = new UmpirskyFileFilter(this.filterDescription, this.filterExtensions);
				this.fileChooser.setAcceptAllFileFilterUsed(false);
				this.fileChooser.setFileFilter(filter);
			}
		}

		// Clear file selection
		File[] emptyFile = {new File("")};
		this.fileChooser.setSelectedFiles(emptyFile);

		// Get files for upload
		int action = this.fileChooser.showOpenDialog(this);
		if (UmpirskyFileChooser.APPROVE_OPTION == action) {
			// Get choice
			ArrayList<File> newFiles = new ArrayList<File>(Arrays.asList(fileChooser.getSelectedFiles()));

			// Remove duplicates
			ArrayList<File> duplicates = new ArrayList<File>();
			if (!files.isEmpty()) {
				for (File file : newFiles) {
					if (files.contains(file)) {
						duplicates.add(file);
					}
				}
				for (File file : duplicates) {
					newFiles.remove(file);
				}
			}

			// Addd them to list
			files.addAll(newFiles);
		}
	}

	/**
	 * Uploads files to server
	 *
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	private void uploadHandler() throws FileNotFoundException, IOException {
		try {
			// Upload files using HttpClient
			PostMethod postMethod = new PostMethod(this.url);
			Part[] parts = new Part[files.size()];
			int i = 0;
			long bytesToSend = 0;
			for (File file : files) {
				bytesToSend += file.length();
				parts[i++] = new FilePart(file.getName(), file);
			}

			postMethod.setRequestEntity(new UmpirskyRequestEntity(new MultipartRequestEntity(parts, postMethod.getParams()), new UmpirskyProgressListener(this, bytesToSend)));
			HttpClient client = new HttpClient();
			int status = client.executeMethod(postMethod);

			// Clear file list and trigger uploadFinished event
			clearFiles();
			getAppletContext().showDocument(new URL("javascript:umpirsky.events.uploadFinished('" + String.valueOf(status) + "')"));
		} catch (FileNotFoundException e) {
			throw e;
		} catch (IOException e) {
			throw e;
		}
	}

	/**
	 * Called when file list is updated.
	 */
	private void fileListUpdated() {
		try {
			getAppletContext().showDocument(new URL("javascript:umpirsky.fileListUpdated()"));
	    }
	    catch (MalformedURLException e) { }
	}

	/**
	 * Updates layout with upload progress information
	 *
	 * @param o progress listener
	 * @param arg upload progress indicator in percents
	 */
	public void update(Observable o, Object arg) {
		Float progress = Float.parseFloat(arg.toString());

		try {
			getAppletContext().showDocument(new URL("javascript:umpirsky.events.uploadProgressListener('" + progress.shortValue() + "')"));
	    }
	    catch (MalformedURLException e) { }
	}

	/**
	 * Logs exception
	 *
	 * @param e exception object
	 */
	private void log(Exception e) {
		e.printStackTrace();
		try {
			getAppletContext().showDocument(new URL("javascript:javascript:umpirsky.events.onError()"));
	    }
	    catch (MalformedURLException ex) { }
	}
}